﻿using Daemon.Common.Cache;
using Daemon.Data.Infrastructure.Auth;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Daemon.Common.Auth
{
    /// <summary>
    /// client auth cache.
    /// </summary>
    public class ClientAuthCache
    {
        private const string AUTHCACHE_CATEGORYNAME = "AuthInfo";
        private const int AUTHCACHE_EXPIRATION_HOUR = 24;
        private const int RETRY_NUMBER = 3;
        private const int RETRY_INTERVAL_MILLISECOND = 500;

        private readonly IDaemonDistributedCache _daemonDistributedCache;

        private IHttpContextAccessor _httpContextAccessor;

        /// <summary>
        /// Initializes a new instance of the <see cref="ClientAuthCache"/> class.
        /// constractor.
        /// </summary>
        public ClientAuthCache(IDaemonDistributedCache daemonDistributedCache, IHttpContextAccessor httpContextAccessor)
        {
            _daemonDistributedCache = daemonDistributedCache;
            _httpContextAccessor = httpContextAccessor;
        }

        /// <summary>
        /// add client user entity. async version
        /// </summary>
        /// <param name="username">user name.</param>
        /// <param name="authInfo">auth info.</param>
        public void AddClientUserEntryAsync(string username, IAuthInfo authInfo)
        {
            authInfo.UserEntity = authInfo.UserEntity.ToSimple();
            var cacheItem = new CacheWrapper<IAuthInfo>
            {
                RedisExpiredTime = DateTime.Now.AddHours(1),
                CacheData = authInfo,
            };
            bool SetRedisCacheAsync()
            {
                _daemonDistributedCache.HashSet<CacheWrapper<IAuthInfo>>(GetAuthCacheKey(), username.ToLowerInvariant(), cacheItem);
                return true;
            }

            Retry(SetRedisCacheAsync, nameof(AddClientUserEntryAsync), $"Fail to add cache, key = {username}, endpoint = {GetEndpoint()}.");
        }

        /// <summary>
        /// clear client user entity.
        /// </summary>
        /// <param name="clientKey">clientkey.</param>
        /// <param name="username">user name.</param>
        public void ClearClientUserEntry(string clientKey, string username)
        {
            bool DeleteCache()
            {
                _daemonDistributedCache.HashDelete<CacheWrapper<IAuthInfo>>(GetAuthCacheKey(), new string[] { username.ToLowerInvariant() });
                return true;
            }

            Retry(DeleteCache, nameof(ClearClientUserEntry), clientKey, $"Fail to clear cache, key = {username}, endpoint = {GetEndpoint()}.");
        }

        /// <summary>
        /// clear client user entity. async version
        /// </summary>
        /// <param name="clientKey">clientkey.</param>
        /// <param name="username">user name.</param>
        public void ClearClientUserEntryAsync(string clientKey, string username)
        {
            bool DeleteCache()
            {
                return _daemonDistributedCache.HashDelete<IAuthInfo>(GetAuthCacheKey(), new string[] { username.ToLowerInvariant() }) > 0;
            }

            Retry(DeleteCache, nameof(ClearClientUserEntryAsync), clientKey, $"Fail to clear cache, key = {username}, endpoint = {GetEndpoint()}.");
        }

        /// <summary>
        /// clear client user entries.
        /// </summary>
        /// <param name="clientKey"></param>
        /// <param name="usernames"></param>
        public void ClearClientUserEntries(string clientKey, IEnumerable<string> usernames)
        {
            if (usernames == null || !usernames.Any())
            {
                return;
            }

            var userList = usernames.Select(username => username.ToLowerInvariant()).ToList();
            bool DeleteCache()
            {
                _daemonDistributedCache.HashDelete<CacheWrapper<IAuthInfo>>(GetAuthCacheKey(), userList);
                return true;
            }

            Retry(DeleteCache, nameof(ClearClientUserEntries), clientKey, $"Fail to clear caches, keys = {string.Join(",", usernames)}, endpoint = {GetEndpoint()}.");
        }

        /// <summary>
        /// get auth info by client user.
        /// </summary>
        /// <param name="username">user name.</param>
        /// <returns>auth info, hasRedisError.</returns>
        public IAuthInfo GetAuthInfoByClientUser(string username)
        {
            AuthInfoCache GetCache()
            {
                return _daemonDistributedCache.HashGet<AuthInfoCache>(GetAuthCacheKey(), username?.ToLowerInvariant());
            }

            var cache = Retry(GetCache, nameof(GetAuthInfoByClientUser), $"Fail to get cache, key = {username}, endpoint = {GetEndpoint()}.");

            if (cache != null && (DateTime.Now - cache.RedisExpiredTime) <= TimeSpan.FromHours(AUTHCACHE_EXPIRATION_HOUR))
            {
                return cache.CacheData;
            }

            return null;
        }

        public Dictionary<string, CacheWrapper<AuthInfoEF>> GetAuthInfosByClientUsers(string clientkey, List<string> usernames)
        {
            CacheWrapper<AuthInfoEF> GetCache(string username)
            {
                return _daemonDistributedCache.HashGet<CacheWrapper<AuthInfoEF>>(GetAuthCacheKey(), username.ToLowerInvariant());
            }

            Dictionary<string, CacheWrapper<AuthInfoEF>> authInfos = new Dictionary<string, CacheWrapper<AuthInfoEF>>();

            foreach (string username in usernames)
            {
                CacheWrapper<AuthInfoEF> cache = Retry(() => GetCache(username), nameof(GetAuthInfosByClientUsers), clientkey, $"Fail to get cache, key = {username}, endpoint = {GetEndpoint()}.");

                if (cache != null && (DateTime.Now - cache.RedisExpiredTime) <= TimeSpan.FromHours(AUTHCACHE_EXPIRATION_HOUR))
                {
                    authInfos.Add(username, cache);
                }
            }

            return authInfos;
        }

        public void UpdateAuthInfoCache(string clientkey, string username, CacheWrapper<AuthInfoEF> authInfoCache)
        {
            bool SetRedisCache()
            {
                _daemonDistributedCache.HashSet(GetAuthCacheKey(), username.ToLowerInvariant(), authInfoCache);
                return true;
            }

            Retry(SetRedisCache, nameof(UpdateAuthInfoCache), clientkey, $"Fail to update cache, key = {clientkey}, endpoint = {GetEndpoint()}.");
        }

        private string GetAuthCacheKey()
        {
            return $"{AUTHCACHE_CATEGORYNAME}";
        }

        private T Retry<T>(Func<T> action, string functionName, string errorMsg)
        {
            var tries = 0;
            try
            {
                while (tries < RETRY_NUMBER)
                {
                    try
                    {
                        return action();
                    }
                    catch (Exception ex)
                    {
                        tries++;
                        if (tries == RETRY_NUMBER)
                        {
                            throw ex;
                        }
                    }

                    Task.Delay(RETRY_INTERVAL_MILLISECOND);
                }
            }
            catch (Exception ex)
            {

            }

            return default;
        }

        private T Retry<T>(Func<T> func, string functionName, string clientKey, string errorMsg)
        {
            var tries = 0;
            try
            {
                while (tries < RETRY_NUMBER)
                {
                    try
                    {
                        return func();
                    }
                    catch (Exception ex)
                    {
                        tries++;
                        if (tries == RETRY_NUMBER)
                        {
                            throw ex;
                        }

                    }

                    Thread.Sleep(RETRY_INTERVAL_MILLISECOND);
                }
            }
            catch (Exception ex)
            {
            }

            return default;
        }

        private string GetEndpoint()
        {
            return _httpContextAccessor.HttpContext.Request.Method + ": " + _httpContextAccessor.HttpContext.GetEndpoint().DisplayName;
        }
    }

    public sealed class AuthInfoCache
    {
        public AuthInfoEF CacheData { get; set; } = null;

        public System.DateTime RedisExpiredTime { get; set; } = System.DateTime.UtcNow;
    }


    public class CacheWrapper<T>
    {
        public DateTime RedisExpiredTime { get; set; }

        public T CacheData { get; set; }
    }
}
